1 module unit_threaded.randomized.benchmark; 2 3 import unit_threaded.from; 4 5 /* This function used $(D MonoTimeImpl!(ClockType.precise).currTime) to time 6 how long $(D MonoTimeImpl!(ClockType.precise).currTime) takes to return 7 the current time. 8 */ 9 private auto medianStopWatchTime() { 10 import core.time; 11 import std.algorithm : sort; 12 import std.datetime : Duration, MonoTimeImpl; 13 14 enum numRounds = 51; 15 Duration[numRounds] times; 16 17 MonoTimeImpl!(ClockType.precise) dummy; 18 for (size_t i = 0; i < numRounds; ++i) { 19 auto sw = MonoTimeImpl!(ClockType.precise).currTime; 20 dummy = MonoTimeImpl!(ClockType.precise).currTime; 21 dummy = MonoTimeImpl!(ClockType.precise).currTime; 22 doNotOptimizeAway(dummy); 23 times[i] = MonoTimeImpl!(ClockType.precise).currTime - sw; 24 } 25 26 sort(times[]); 27 28 return times[$ / 2].total!"hnsecs"; 29 } 30 31 private from!"std.datetime".Duration getQuantilTick(double q)( 32 from!"std.datetime".Duration[] ticks) pure @safe { 33 size_t idx = cast(size_t)(ticks.length * q); 34 35 if (ticks.length % 2 == 1) { 36 return ticks[idx]; 37 } else { 38 return (ticks[idx] + ticks[idx - 1]) / 2; 39 } 40 } 41 42 // @Name("Quantil calculations") 43 // unittest 44 // { 45 // static import std.conv; 46 // import std.algorithm.iteration : map; 47 48 // auto ticks = [1, 2, 3, 4, 5].map!(a => dur!"seconds"(a)).array; 49 50 // Duration q25 = getQuantilTick!0.25(ticks); 51 // assert(q25 == dur!"seconds"(2), q25.toString()); 52 53 // Duration q50 = getQuantilTick!0.50(ticks); 54 // assert(q50 == dur!"seconds"(3), q25.toString()); 55 56 // Duration q75 = getQuantilTick!0.75(ticks); 57 // assert(q75 == dur!"seconds"(4), q25.toString()); 58 59 // q25 = getQuantilTick!0.25(ticks[0 .. 4]); 60 // assert(q25 == dur!"seconds"(1) + dur!"msecs"(500), q25.toString()); 61 62 // q50 = getQuantilTick!0.50(ticks[0 .. 4]); 63 // assert(q50 == dur!"seconds"(2) + dur!"msecs"(500), q25.toString()); 64 65 // q75 = getQuantilTick!0.75(ticks[0 .. 4]); 66 // assert(q75 == dur!"seconds"(3) + dur!"msecs"(500), q25.toString()); 67 // } 68 69 /** The options controlling the behaviour of benchmark. */ 70 struct BenchmarkOptions { 71 import std.datetime : Duration, seconds; 72 73 string funcname; // the name of the function to benchmark 74 string filename; // the name of the file the results will be appended to 75 Duration duration = 1.seconds; // the time after which the function to 76 // benchmark is not executed anymore 77 size_t maxRounds = 10000; // the maximum number of times the function 78 // to benchmark is called 79 int seed = 1337; // the seed to the random number generator 80 81 this(string funcname) { 82 this.funcname = funcname; 83 } 84 } 85 86 /** This $(D struct) takes care of the time taking and outputting of the 87 statistics. 88 */ 89 struct Benchmark { 90 import std.array : Appender; 91 import std.datetime : Duration, MonoTimeImpl, ClockType; 92 93 string filename; // where to write the benchmark result to 94 string funcname; // the name of the benchmark 95 size_t rounds; // the number of times the functions is supposed to be 96 //executed 97 string timeScale; // the unit the benchmark is measuring in 98 real medianStopWatch; // the median time it takes to get the clocktime twice 99 bool dontWrite; // if set, no data is written to the the file name "filename" 100 // true if, RndValueGen opApply was interrupt unexpectitally 101 Appender!(Duration[]) ticks; // the stopped times, there will be rounds ticks 102 size_t ticksIndex = 0; // the index into ticks 103 size_t curRound = 0; // the number of rounds run 104 MonoTimeImpl!(ClockType.precise) startTime; 105 Duration timeSpend; // overall time spend running the benchmark function 106 107 /** The constructor for the $(D Benchmark). 108 Params: 109 funcname = The name of the $(D benchmark) instance. The $(D funcname) 110 will be used to associate the results with the function 111 founds = How many rounds. 112 filename = The $(D filename) will be used as a filename to store the 113 results. 114 */ 115 this(in string funcname, in size_t rounds, in string filename) { 116 import std.array : appender; 117 118 this.filename = filename; 119 this.funcname = funcname; 120 this.rounds = rounds; 121 this.timeScale = "hnsecs"; 122 this.ticks = appender!(Duration[])(); 123 this.medianStopWatch = medianStopWatchTime(); 124 } 125 126 /** A call to this method will start the time taking process */ 127 void start() { 128 this.startTime = MonoTimeImpl!(ClockType.precise).currTime; 129 } 130 131 /** A call to this method will stop the time taking process, and 132 appends the execution time to the $(D ticks) member. 133 */ 134 void stop() { 135 auto end = MonoTimeImpl!(ClockType.precise).currTime; 136 Duration dur = end - this.startTime; 137 this.timeSpend += dur; 138 this.ticks.put(dur); 139 ++this.curRound; 140 } 141 142 ~this() { 143 import std.stdio : File; 144 import std.datetime : Clock; 145 146 if (!this.dontWrite && this.ticks.data.length) { 147 import std.algorithm : sort; 148 149 auto sortedTicks = this.ticks.data; 150 sortedTicks.sort(); 151 152 auto f = File(filename ~ "_bechmark.csv", "a"); 153 scope (exit) 154 f.close(); 155 156 auto q0 = sortedTicks[0].total!("hnsecs")() / cast(double) this.rounds; 157 auto q25 = getQuantilTick!0.25(sortedTicks).total!("hnsecs")() / cast(double) this 158 .rounds; 159 auto q50 = getQuantilTick!0.50(sortedTicks).total!("hnsecs")() / cast(double) this 160 .rounds; 161 auto q75 = getQuantilTick!0.75(sortedTicks).total!("hnsecs")() / cast(double) this 162 .rounds; 163 auto q100 = sortedTicks[$ - 1].total!("hnsecs")() / cast(double) this.rounds; 164 165 // funcname, the data when the benchmark was created, unit of time, 166 // rounds, medianStopWatch, low, 0.25 quantil, median, 167 // 0.75 quantil, high 168 f.writefln("\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"" ~ ",\"%s\"", this.funcname, 169 Clock.currTime.toISOExtString(), this.timeScale, this.curRound, 170 this.medianStopWatch, q0 > this.medianStopWatch ? q0 - this.medianStopWatch : 0, 171 q25 > this.medianStopWatch ? q25 - this.medianStopWatch : 0, q50 > this.medianStopWatch 172 ? q50 - this.medianStopWatch : 0, q75 > this.medianStopWatch ? q75 - this.medianStopWatch 173 : 0, q100 > this.medianStopWatch ? q100 - this.medianStopWatch : 0); 174 } 175 } 176 } 177 178 void doNotOptimizeAway(T...)(ref T t) { 179 foreach (ref it; t) { 180 doNotOptimizeAwayImpl(&it); 181 } 182 } 183 184 private void doNotOptimizeAwayImpl(void* p) { 185 import core.thread : getpid; 186 import std.stdio : writeln; 187 188 if (getpid() == 1) { 189 writeln(*cast(char*) p); 190 } 191 } 192 193 // unittest 194 // { 195 // static void funToBenchmark(int a, float b, Gen!(int, -5, 5) c, string d, 196 // GenASCIIString!(1, 10) e) 197 // { 198 // import core.thread; 199 200 // Thread.sleep(1.seconds / 100000); 201 // doNotOptimizeAway(a, b, c, d, e); 202 // } 203 204 // benchmark!funToBenchmark(); 205 // benchmark!funToBenchmark("Another Name"); 206 // benchmark!funToBenchmark("Another Name", 2.seconds); 207 // benchmark!funToBenchmark(2.seconds); 208 // } 209 210 /** This function runs the passed callable $(D T) for the duration of 211 $(D maxRuntime). It will count how often $(D T) is run in the duration and 212 how long each run took to complete. 213 214 Unless compiled in release mode, statistics will be printed to $(D stderr). 215 If compiled in release mode the statistics are appended to a file called 216 $(D name). 217 218 Params: 219 opts = A $(D BenchmarkOptions) instance that encompasses all possible 220 parameters of benchmark. 221 name = The name of the benchmark. The name is also used as filename to 222 save the benchmark results. 223 maxRuntime = The maximum time the benchmark is executed. The last run will 224 not be interrupted. 225 rndSeed = The seed to the random number generator used to populate the 226 parameter passed to the function to benchmark. 227 rounds = The maximum number of times the callable $(D T) is called. 228 */ 229 void benchmark(alias T)(const ref BenchmarkOptions opts) { 230 import std.random : Random; 231 import std.traits : ParameterIdentifierTuple, Parameters; 232 import unit_threaded.randomized.random; 233 234 auto bench = Benchmark(opts.funcname, opts.maxRounds, opts.filename); 235 auto rnd = Random(opts.seed); 236 enum string[] parameterNames = [ParameterIdentifierTuple!T]; 237 auto valueGenerator = RndValueGen!(parameterNames, Parameters!T)(&rnd); 238 239 while (bench.timeSpend <= opts.duration && bench.curRound < opts.maxRounds) { 240 valueGenerator.genValues(); 241 242 bench.start(); 243 try { 244 T(valueGenerator.values); 245 } catch (Throwable t) { 246 import std.experimental.logger : logf; 247 248 logf("unittest with name %s failed when parameter %s where passed", 249 opts.funcname, valueGenerator); 250 break; 251 } finally { 252 bench.stop(); 253 ++bench.curRound; 254 } 255 } 256 } 257 258 /// Ditto 259 void benchmark(alias T)(string funcname = "", string filename = __FILE__) { 260 import std..string : empty; 261 import std.traits : fullyQualifiedName; 262 263 auto opt = BenchmarkOptions(funcname.empty ? fullyQualifiedName!T : funcname); 264 opt.filename = filename; 265 benchmark!(T)(opt); 266 } 267 268 /// Ditto 269 void benchmark(alias T)(from!"std.datetime".Duration maxRuntime, string filename = __FILE__) { 270 import std.traits : fullyQualifiedName; 271 272 auto opt = BenchmarkOptions(fullyQualifiedName!T); 273 opt.filename = filename; 274 opt.duration = maxRuntime; 275 benchmark!(T)(opt); 276 } 277 278 /// Ditto 279 /*void benchmark(alias T)(string name, string filename = __FILE__) 280 { 281 auto opt = BenchmarkOptions(name); 282 opt.filename = filename; 283 benchmark!(T)(opt); 284 }*/ 285 286 /// Ditto 287 void benchmark(alias T)(string name, from!"std.datetime".Duration maxRuntime, 288 string filename = __FILE__) { 289 auto opt = BenchmarkOptions(name); 290 opt.filename = filename; 291 opt.duration = maxRuntime; 292 benchmark!(T)(opt); 293 }